package top.fullj.eventbus;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executor;

/**
 * @author bruce.wu
 * @since 2022/2/24 14:59
 */
public class EventBus {

    private static volatile EventBus DEFAULT;

    public static EventBus getDefault() {
        if (DEFAULT == null) {
            synchronized (EventBus.class) {
                if (DEFAULT == null) {
                    DEFAULT = new EventBus();
                }
            }
        }
        return DEFAULT;
    }

    private static final Map<String, Executor> EXECUTOR_MAP = new HashMap<>();

    static {
        registerExecutors(EventExecutors.immediate(),
                EventExecutors.newThread(), EventExecutors.single(),
                EventExecutors.pool(), EventExecutors.ui());

        for (EventExecutor instance : ServiceLoader.load(EventExecutor.class)) {
            EXECUTOR_MAP.put(instance.threadMode(), instance);
            System.out.println("EventBus.executor[" + instance.threadMode() + "] = " + instance.getClass().getName());
        }
    }

    public static void registerExecutors(EventExecutor...executors) {
        for (EventExecutor instance : executors) {
            EXECUTOR_MAP.put(instance.threadMode(), instance);
            System.out.println("EventBus.Executor[" + instance.threadMode() + "] = " + instance.getClass().getName());
        }
    }

    private final Registry registry = new Registry();
    private final Map<Class<?>, CopyOnWriteArraySet<Object>> stickyEvents = new ConcurrentHashMap<>();

    private EventBus() {
    }

    public void register(Object target) {
        registry.register(target, this::subscribeOnSticky);
    }

    public void unregister(Object target) {
        registry.unregister(target);
    }

    private void subscribeOnSticky(Subscriber subscriber) {
        CopyOnWriteArraySet<Object> events = stickyEvents.get(subscriber.eventType);
        if (events != null) {
            for (Object event: events) {
                execute(event, subscriber);
            }
        }
    }

    public void postSticky(Object event) {
        Class<?> type = event.getClass();
        CopyOnWriteArraySet<Object> events = stickyEvents.get(type);
        if (events == null) {
            synchronized (stickyEvents) {
                events = Maps.putIfAbsent(stickyEvents, type, new CopyOnWriteArraySet<>());
            }
        }
        events.add(event);

        post(event, true);
    }

    @SuppressWarnings("UnusedReturnValue")
    public boolean removeStickyEvent(Object event) {
        Class<?> type = event.getClass();
        CopyOnWriteArraySet<Object> events = stickyEvents.get(type);
        if (events == null) {
            return false;
        }
        return events.remove(event);
    }

    @SuppressWarnings("UnusedReturnValue")
    public Set<Object> removeStickyEvents(Class<?> type) {
        return stickyEvents.remove(type);
    }

    public void removeStickyEvents() {
        stickyEvents.clear();
    }

    public void post(Object event) {
        post(event, false);
    }

    private void post(Object event, boolean sticky) {
        Set<Subscriber> subscribers = registry.getSubscribers(event.getClass());
        if (subscribers.isEmpty()) {
            if (!(event instanceof DeadEvent) && !sticky) {
                post(new DeadEvent(this, event));
            }
        } else {
            for (Subscriber subscriber : subscribers) {
                execute(event, subscriber);
            }
        }
    }

    private void execute(Object event, Subscriber subscriber) {
        final Method method = subscriber.method;
        final Object target = subscriber.target;
        final Executor executor = findExecutorByThreadMode(subscriber.threadMode);
        final Runnable task = EventBusPlugins.onExecute(() -> {
            try {
                method.setAccessible(true);
            } catch (Throwable ignored) {

            }
            try {
                method.invoke(target, event);
            } catch (ReflectiveOperationException e) {
                EventBusPlugins.onError(this, event, target, method, e);
            }
        });
        executor.execute(task);
        if (subscriber.once) {
            registry.unsubscribe(subscriber);
        }
    }

    private Executor findExecutorByThreadMode(String threadMode) {
        Executor executor = EXECUTOR_MAP.get(threadMode);
        if (executor == null) {
            throw new IllegalArgumentException("Cant execute in: " + threadMode);
        }
        return executor;
    }

}
